package ru.yandex.market.graphouse.cacher; import com.google.common.annotations.VisibleForTesting; import com.google.common.io.LittleEndianDataOutputStream; import org.apache.http.entity.AbstractHttpEntity; import ru.yandex.market.graphouse.Metric; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.List; /** * Write in ClickHouse in a fast RowBinary format * See https://clickhouse.yandex/reference_en.html#RowBinary for details * @author Dmitry Andreev <a href="mailto:AndreevDm@yandex-team.ru"></a> * @date 16/04/2017 */ public class MetricRowBinaryHttpEntity extends AbstractHttpEntity { private static final LocalDate EPOCH = LocalDate.ofEpochDay(0); private final int todayStartSeconds; private final int todayEndSeconds; private final short currentDay; private final List<Metric> metrics; @VisibleForTesting protected MetricRowBinaryHttpEntity(List<Metric> metrics, LocalDate localDate) { this.metrics = metrics; //Optimization. Assume that all metrics are today and precalc day number. currentDay = (short) Short.toUnsignedInt((short) localDate.toEpochDay()); todayStartSeconds = (int) localDate.atStartOfDay(ZoneId.systemDefault()).toEpochSecond(); todayEndSeconds = (int) localDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toEpochSecond(); } public MetricRowBinaryHttpEntity(List<Metric> metrics) { this(metrics, LocalDate.now()); } @Override public boolean isRepeatable() { return true; } @Override public long getContentLength() { return -1; } @Override public InputStream getContent() throws IOException, UnsupportedOperationException { throw new UnsupportedOperationException(); } @Override public void writeTo(OutputStream outputStream) throws IOException { LittleEndianDataOutputStream out = new LittleEndianDataOutputStream(outputStream); for (Metric metric : metrics) { writeMetric(metric, out); } } /** * (metric, value, timestamp, date, updated) * * @param metric * @param out * @throws IOException */ private void writeMetric(Metric metric, LittleEndianDataOutputStream out) throws IOException { writeUnsignedLeb128(metric.getMetricDescription().getNameLength(), out); metric.getMetricDescription().writeName(out); out.writeDouble(metric.getValue()); out.writeInt((int) Integer.toUnsignedLong(metric.getTimestampSeconds())); out.writeShort(getUnsignedDaysSinceEpoch(metric.getTimestampSeconds())); out.writeInt((int) Integer.toUnsignedLong(metric.getUpdatedSeconds())); } @VisibleForTesting protected short getUnsignedDaysSinceEpoch(int timestampSeconds) { if (timestampSeconds >= todayStartSeconds && timestampSeconds < todayEndSeconds) { return currentDay; } LocalDate localDate = Instant.ofEpochSecond(timestampSeconds).atZone(ZoneId.systemDefault()).toLocalDate(); short days = (short) ChronoUnit.DAYS.between(EPOCH, localDate); return (short) Short.toUnsignedInt(days); } private static void writeUnsignedLeb128(int value, LittleEndianDataOutputStream out) throws IOException { int remaining = value >>> 7; while (remaining != 0) { out.write((byte) ((value & 0x7f) | 0x80)); value = remaining; remaining >>>= 7; } out.write((byte) (value & 0x7f)); } @Override public boolean isStreaming() { return false; } @VisibleForTesting protected int getTodayStartSeconds() { return todayStartSeconds; } @VisibleForTesting protected int getTodayEndSeconds() { return todayEndSeconds; } @VisibleForTesting protected short getCurrentDay() { return currentDay; } }